﻿/////////////////////////////////////////////////////////////////////////////////////////////////////
// Copyright (c) 2009 Eric Bowen
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and 
// associated documentation files (the "Software"), to deal in the Software without restriction, including 
// without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 
// copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the 
// following conditions:
//
// The above copyright notice and this permission notice shall be included in all copies or substantial 
// portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT 
// LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN 
// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 
// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 
// SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
/////////////////////////////////////////////////////////////////////////////////////////////////////
// Author's blog: http://blog.scrappydog.com
// Updated source code available:  http://scrappydb.codeplex.com
/////////////////////////////////////////////////////////////////////////////////////////////////////

using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Reflection;
using System.Web;
using System.Web.Caching;
using System.Web.Hosting;
using System.Web.Security;
using NLog;
using ScrappyDB.BaseClasses;

namespace ScrappyDB.Membership
{
    public sealed class SdbRoleProvider : RoleProvider
    {
        // Create a logger for use in this class
        private static readonly Logger log = LogManager.GetCurrentClassLogger();

        private Cache _cache;
        private MembershipCommon _common;
        private Db _db;

        public override string ApplicationName { get; set; }

        #region  ******************* Overides To System.Web.Security.RoleProvider Methods *******************

        //
        // RoleProvider.AddUsersToRoles
        //
        public override void AddUsersToRoles(string[] usernames, string[] rolenames)
        {
            IEnumerable<Guid> roleIds = GetRoleIds(rolenames);
            IEnumerable<Guid> userIds = GetUserIds(usernames);

            foreach (Guid userId in userIds)
            {
                foreach (Guid roleId in roleIds)
                {
                    var userInRole = new UserInRole
                                         {RoleId = roleId, UserId = userId, UserInRoleId = userId + "_" + roleId};
                    //this record may already exist, but we can just resave it because the UserInRoleId is unique
                    _db.SaveChanges(userInRole);
                }
            }
        }

        //
        // RoleProvider.CreateRole
        //
        public override void CreateRole(string rolename)
        {
            if (rolename.Contains(","))
                throw new SdbRoleProviderException("Role names cannot contain commas.");

            if (RoleExists(rolename))
                throw new SdbRoleProviderException("Role name already exists.");

            var role = new Role
                           {
                               RoleId = Guid.NewGuid(),
                               RoleName = rolename,
                               ApplicationName = ApplicationName
                           };

            _db.SaveChanges(role);
        }

        //
        // RoleProvider.DeleteRole
        //
        public override bool DeleteRole(string rolename, bool throwOnPopulatedRole)
        {
            if (!RoleExists(rolename))
            {
                //throw new SdbRoleProviderException("Role does not exist.");
                return false;
            }

            if (throwOnPopulatedRole && GetUsersInRole(rolename).Length > 0)
                throw new SdbRoleProviderException("Cannot delete a populated role.");

            SdbCollection<Role> roles =
                _db.Query<Role>(" ApplicationName = '" + ApplicationName + "' and RoleName = '" + rolename + "' ", true);

            foreach (Role role in roles)
            {
                //remove related UserInRoleRecords
                SdbCollection<UserInRole> usersInRoles =
                    _db.Query<UserInRole>(" ApplicationName = '" + ApplicationName + "' and RoleId = '" + role.RoleId +
                                          "' ", true);

                foreach (UserInRole userInRole in usersInRoles)
                    _db.Delete(userInRole);

                //remove role
                _db.Delete(role);
            }

            return true;
        }

        public override string[] FindUsersInRole(string rolename, string usernameToMatch)
        {
            throw new NotImplementedException();
        }

        //
        // RoleProvider.GetAllRoles
        //
        public override string[] GetAllRoles()
        {
            string tmpRoleNames = "";

            SdbCollection<Role> roles = _db.Query<Role>(" ApplicationName = '" + ApplicationName + "' ", true);

            foreach (Role role in roles)
                tmpRoleNames += role.RoleName + ",";

            if (tmpRoleNames.Length > 0)
            {
                // Remove trailing comma.
                tmpRoleNames = tmpRoleNames.Substring(0, tmpRoleNames.Length - 1);
                return tmpRoleNames.Split(',');
            }

            return new string[0];
        }

        public override string[] GetRolesForUser(string username)
        {
            Dictionary<Guid, string> roleDictionary = GetRoleIdDictionary(_cache);
            User user = _common.GetUserByUsernameOrEmail(username);

            SdbCollection<UserInRole> userInRoles = _db.Query<UserInRole>(" UserId = '" + user.UserId + "' ", true);

            var result = new List<string>();

            foreach (UserInRole id in userInRoles)
            {
                string roleName;
                if (!roleDictionary.TryGetValue(id.RoleId, out roleName))
                    throw new SdbRoleProviderException("RoleId: '" + id.RoleId + "' not found");
                result.Add(roleName);
            }

            return result.ToArray();
        }

        public override string[] GetUsersInRole(string rolename)
        {
            //TODO: need to implement some sort of paging for this

            Dictionary<string, Guid> roleNameDictionary = GetRoleNameDictionary(_cache);
            Guid roleId;

            if (!roleNameDictionary.TryGetValue(rolename, out roleId))
                throw new SdbRoleProviderException("Role: '" + rolename + "' not found");

            SdbCollection<UserInRole> userInRoles = _db.Query<UserInRole>(" RoleId = '" + roleId + "' ", true);

            if (userInRoles.Count == 0)
                return new string[0];

            var userNames = new List<string>();
            var users = new SdbCollection<User>();

            foreach (UserInRole userInRole in userInRoles)
                users.AddById(userInRole.UserId.ToString());

            foreach (User user in users)
                userNames.Add(user.UserName);

            return userNames.ToArray();
        }

        public override bool IsUserInRole(string username, string rolename)
        {
            Dictionary<string, Guid> roleNameDictionary = GetRoleNameDictionary(_cache);

            Guid roleId;

            if (!roleNameDictionary.TryGetValue(rolename, out roleId))
                throw new SdbRoleProviderException("Role: '" + rolename + "' not found");

            var user = _common.GetUserByUsernameOrEmail(username);
            var userId = user.UserId;

            bool isUserInRole = false;

            var tempUserInRole = new UserInRole {RoleId = roleId, UserId = userId};

            var userInRole = _db.Find<UserInRole>(tempUserInRole.UserInRoleId);

            if (userInRole != null)
                isUserInRole = true;

            return isUserInRole;
        }

        public override void RemoveUsersFromRoles(string[] usernames, string[] rolenames)
        {
            IEnumerable<Guid> roleIds = GetRoleIds(rolenames);
            IEnumerable<Guid> userIds = GetUserIds(usernames);

            foreach (Guid userId in userIds)
            {
                foreach (Guid roleId in roleIds)
                {
                    var userInRole = new UserInRole {RoleId = roleId, UserId = userId};
                    _db.Delete(userInRole);
                }
            }
        }

        public override bool RoleExists(string rolename)
        {
            bool exists = false;

            SdbCollection<Role> roles =
                _db.Query<Role>(" ApplicationName = '" + ApplicationName + "' and RoleName = '" + rolename + "' ", true);

            if (roles != null && roles.Count > 0)
                exists = true;

            return exists;
        }

        public void AddUsersToRoles(string[] usernames, string[] rolenames, Cache cache)
        {
            _cache = cache;
            AddUsersToRoles(usernames, rolenames);
        }

        public void CreateRole(string rolename, Cache cache)
        {
            _cache = cache;
            CreateRole(rolename);
        }

        public bool DeleteRole(string rolename, bool throwOnPopulatedRole, Cache cache)
        {
            _cache = cache;
            return DeleteRole(rolename, throwOnPopulatedRole);
        }

        public string[] GetAllRoles(Cache cache)
        {
            _cache = cache;
            return GetAllRoles();
        }

        public string[] GetRolesForUser(string username, Cache cache)
        {
            _cache = cache;
            return GetRolesForUser(username);
        }

        public string[] GetUsersInRole(string rolename, Cache cache)
        {
            _cache = cache;
            return GetUsersInRole(rolename);
        }

        public bool IsUserInRole(string username, string rolename, Cache cache)
        {
            _cache = cache;
            return IsUserInRole(username, rolename);
        }

        public bool RoleExists(string rolename, Cache cache)
        {
            _cache = cache;
            return RoleExists(rolename);
        }

        #endregion

        #region  ******************* New (Non-Standard) Public RoleProvider Methods *******************

        public Dictionary<Guid, string> GetRoleIdDictionary(Cache cache)
        {
            _cache = cache;

            SdbCollection<Role> roles = _db.Query<Role>(" ApplicationName = '" + ApplicationName + "' ", true);

            var result = new Dictionary<Guid, string>();

            foreach (Role role in roles)
                result.Add(role.RoleId, role.RoleName);

            return result;
        }

        public Dictionary<string, Guid> GetRoleNameDictionary(Cache cache)
        {
            _cache = cache;

            SdbCollection<Role> roles = _db.Query<Role>(" ApplicationName = '" + ApplicationName + "' ", true);

            var result = new Dictionary<string, Guid>();

            foreach (Role role in roles)
                result.Add(role.RoleName, role.RoleId);

            return result;
        }

        #endregion

        #region  ******************* Private Methods *******************

        private IEnumerable<Guid> GetRoleIds(IEnumerable<string> rolenames)
        {
            var roleIds = new List<Guid>();

            Dictionary<string, Guid> roleDictionary = GetRoleNameDictionary(_cache);

            foreach (string rolename in rolenames)
            {
                Guid id;
                if (!roleDictionary.TryGetValue(rolename, out id))
                    throw new SdbRoleProviderException("Rolename: '" + rolename + "' doesn't exist");

                roleIds.Add(id);
            }
            return roleIds;
        }

        private IEnumerable<Guid> GetUserIds(IEnumerable<string> usernames)
        {
            var userIds = new List<Guid>();
            foreach (string username in usernames)
            {
                if (username.Contains(","))
                    throw new SdbRoleProviderException("User names cannot contain commas.");
                User user = _common.GetUserByUsernameOrEmail(username);

                if (user == null)
                    throw new SdbRoleProviderException("User: '" + username + "' not found");

                userIds.Add(user.UserId);

                //I don't think there is any point to this check
                //foreach (string rolename in rolenames)
                //{
                //    if (IsUserInRole(username, rolename))
                //    {
                //        throw new SdbRoleProviderException("User is already in role.");
                //    }
                //}
            }
            return userIds;
        }

        //private User GetDbUser(string username)
        //{
        //    ScrappyDbCollection<User> users = _db.Query<User>(" UserName = '" + username + "' ",true);

        //    //TODO: log the exception instead if appropriate
        //    if (users.Count != 1)
        //    {
        //        Log.Warn("WARNING: User: '" + username + "' not valid");
        //        return null;
        //    }

        //    User user = users[0];

        //    return user;
        //}

        #endregion

        /// <summary>
        /// System.Configuration.Provider.ProviderBase.Initialize Method
        /// </summary>
        /// <param name="name"></param>
        /// <param name="config"></param>
        public override void Initialize(string name, NameValueCollection config)
        {
            //
            // Initialize values from web.config.
            //

            if (config == null)
                throw new ArgumentNullException("config");

            if (string.IsNullOrEmpty(name))
                name = "SdbRoleProvider";

            if (string.IsNullOrEmpty(config["description"]))
            {
                config.Remove("description");
                config.Add("description", "ScrappyDB RoleProvider");
            }

            // Initialize the abstract base class.
            base.Initialize(name, config);

            //Debugger.Break();

            if (string.IsNullOrEmpty(config["applicationName"]))
            {
                if (!string.IsNullOrEmpty(HostingEnvironment.ApplicationVirtualPath))
                    ApplicationName = HostingEnvironment.ApplicationVirtualPath;
                else
                    ApplicationName = "SdbRoleProvider";
            }
            else
                ApplicationName = config["applicationName"];

            //
            // Initialize SimpleDB.
            //
            Cache cache = null;
            if (HttpContext.Current != null)
                cache = HttpContext.Current.Cache;
            _db = new Db(cache);

            _common = new MembershipCommon(_db);
        }
    }
}